<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
    />
    <meta
      name="description"
      content="Mars terrain visualized using 3D Tiles, tiled and hosted by Cesium ion, and shown with points of interest and additional data layers."
    />
    <meta name="cesium-sandcastle-labels" content="Showcases, ion Assets, 3D Tiles" />
    <title>Cesium Mars</title>
    <script type="text/javascript" src="../Sandcastle-header.js"></script>
    <script
      type="text/javascript"
      src="../../../Build/CesiumUnminified/Cesium.js"
      nomodule
    ></script>
    <script type="module" src="../load-cesium-es6.js"></script>
  </head>
  <body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
    <style>
      @import url(../templates/bucket.css);
      #toolbar {
        background: rgba(42, 42, 42, 0.8);
        padding: 4px;
        border-radius: 4px;
      }
      #toolbar input {
        vertical-align: middle;
        padding-top: 2px;
        padding-bottom: 2px;
      }
      #toolbar .header {
        font-weight: bold;
      }

      /* Styles for indicating to the user to press the play button */
      .cesium-animation-rectButton.highlight-animation .cesium-animation-buttonGlow {
        display: block;
        fill: #fff;
        /* keep the built-in blur and add extra glow layers */
        filter: url(#animation_blurred) drop-shadow(0 0 3px #aef)
          drop-shadow(0 0 3px #fff);
        animation: highlight-animation-button 1.2s ease-in-out infinite;
      }
      .cesium-animation-rectButton.highlight-animation .cesium-animation-buttonMain {
        stroke: #fff;
        stroke-width: 3;
      }
      .cesium-animation-rectButton.highlight-animation .cesium-animation-buttonPath {
        fill: #fff;
      }

      .cesium-animation-shuttleRingG.highlight-animation
        .cesium-animation-shuttleRingBack {
        stroke: #fff;
        stroke-width: 6;
        filter: drop-shadow(0 0 3px #aef) drop-shadow(0 0 3px rgba(174, 238, 255, 0.95));
        animation: highlight-animation-ring 1.2s ease-in-out infinite;
      }
      .cesium-animation-shuttleRingG.highlight-animation
        .cesium-animation-shuttleRingSwoosh
        line {
        stroke: #fff;
        stroke-opacity: 0.8;
      }

      @keyframes highlight-animation-button {
        0% {
          opacity: 0.3;
          stroke-width: 0;
        }
        50% {
          opacity: 1;
          stroke-width: 4;
        }
        100% {
          opacity: 0.3;
          stroke-width: 0;
        }
      }
      @keyframes highlight-animation-ring {
        0% {
          stroke-opacity: 0.25;
          stroke: #333;
        }
        50% {
          stroke-opacity: 1;
          stroke: #fff;
        }
        100% {
          stroke-opacity: 0.25;
          stroke: #333;
        }
      }
    </style>
    <div id="cesiumContainer" class="fullSize"></div>
    <div id="loadingOverlay"><h1>Loading...</h1></div>
    <div id="toolbar"></div>
    <template id="roverHelpRowTemplate1">
      <tr>
        <td>
          <img
            data-src="Widgets/Images/NavigationHelp/MouseLeft.svg"
            style="height: 48px; width: 48px"
            alt="Left mouse button"
          />
        </td>
        <td>
          <div class="cesium-navigation-help-pan">Track Rover</div>
          <div class="cesium-navigation-help-detail">
            Double click on a rover to track it
          </div>
        </td>
      </tr>
    </template>
    <template id="roverHelpRowTemplate2">
      <tr>
        <td>
          <svg width="48" height="48" viewBox="0 0 32 32" aria-label="Play" role="img">
            <path
              transform="translate(32,32) scale(0.85) translate(-32,-32)"
              d="M6.684,25.682L24.316,15.5L6.684,5.318V25.682z"
              fill="#ffffff"
            />
          </svg>
        </td>
        <td>
          <div class="cesium-navigation-help-zoom">Play Animation</div>
          <div class="cesium-navigation-help-detail">
            Press play on the timeline to watch the rover move
          </div>
        </td>
      </tr>
    </template>
    <script id="cesium_sandcastle_script">
      window.startup = async function (Cesium) {
        "use strict";
        //Sandcastle_Begin

        Cesium.Ellipsoid.default = Cesium.Ellipsoid.MARS;
        const viewer = new Cesium.Viewer("cesiumContainer", {
          terrainProvider: false,
          baseLayer: false,
          baseLayerPicker: false,
          geocoder: false,
          shadows: false,
          globe: new Cesium.Globe(Cesium.Ellipsoid.MARS),
          skyBox: Cesium.SkyBox.createEarthSkyBox(),
          skyAtmosphere: new Cesium.SkyAtmosphere(Cesium.Ellipsoid.MARS),
        });
        viewer.scene.globe.show = false;
        const scene = viewer.scene;
        const clock = viewer.clock;
        const navHelp = viewer.navigationHelpButton;

        // Adjust the default atmosphere coefficients to be more Mars-like
        scene.skyAtmosphere.atmosphereMieCoefficient = new Cesium.Cartesian3(
          9.0e-5,
          2.0e-5,
          1.0e-5,
        );
        scene.skyAtmosphere.atmosphereRayleighCoefficient = new Cesium.Cartesian3(
          9.0e-6,
          2.0e-6,
          1.0e-6,
        );
        scene.skyAtmosphere.atmosphereRayleighScaleHeight = 9000;
        scene.skyAtmosphere.atmosphereMieScaleHeight = 2700.0;
        scene.skyAtmosphere.saturationShift = -0.1;
        scene.skyAtmosphere.perFragmentAtmosphere = true;

        // Adjust postprocess settings for brighter and richer features
        const bloom = viewer.scene.postProcessStages.bloom;
        bloom.enabled = true;
        bloom.uniforms.brightness = -0.5;
        bloom.uniforms.stepSize = 1.0;
        bloom.uniforms.sigma = 3.0;
        bloom.uniforms.delta = 1.5;
        scene.highDynamicRange = true;
        viewer.scene.postProcessStages.exposure = 1.5;

        // Load Mars tileset
        try {
          const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(3644333, {
            enableCollision: true,
          });
          viewer.scene.primitives.add(tileset);
        } catch (error) {
          console.log(error);
        }

        // Load the rovers and path from The Martian (by Andy Weir), from CZML data source.
        let curiosity, perseverance, ingenuity, theMartianJourney;
        try {
          const dataSource = await Cesium.CzmlDataSource.load(
            "../../SampleData/Mars.czml",
          );
          viewer.dataSources.add(dataSource);

          const roverMenuEntries = [
            {
              text: "Fly to rover...",
              onselect: () => {},
            },
          ];

          const onSelectRover = (rover) => {
            reset();
            const roverAnimStartIso = rover.properties.animationStartTime.getValue(
              Cesium.JulianDate.now(),
            );
            clock.multiplier = 604800;
            clock.currentTime = new Cesium.JulianDate.fromIso8601(roverAnimStartIso);
            viewer.timeline.zoomTo(rover.availability.start, rover.availability.stop);

            const boundingSphere = new Cesium.BoundingSphere(
              rover.position.getValue(clock.currentTime),
              5000.0,
            );

            scene.camera.flyToBoundingSphere(boundingSphere, {
              offset: new Cesium.HeadingPitchRoll(4.9791, -0.5294, 0.0),
              easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
              maximumHeight: 5e6,
              pitchAdjustHeight: 2.5e6,
              duration: 3.0,
              complete: function () {
                highlightAnimationViewModel(); // Draw attention to the play button
                navHelp.viewModel.showInstructions = true;
              },
            });
          };

          const setupRover = function (entityId, startSol, outRover) {
            outRover = dataSource.entities.getById(entityId);

            const julianDateToSol = createJulianDateToSolConverter(
              outRover.availability.start,
              startSol,
            );
            outRover.label.text = new Cesium.CallbackProperty(function (time) {
              return julianDateToSol(time);
            }, false);

            const roverPath = dataSource.entities.getById(`${entityId}Path`);
            roverPath.polyline.width = createWidthCallbackProperty(
              new Cesium.NearFarScalar(0.0, 15.0, 1.0e5, 0.0),
            );

            roverMenuEntries.push({
              text: entityId,
              onselect: () => onSelectRover(outRover),
            });

            return outRover;
          };

          curiosity = setupRover("Curiosity", 3, curiosity);
          perseverance = setupRover("Perseverance", 13, perseverance);
          ingenuity = dataSource.entities.getById("Ingenuity"); // Only for viewing - no data for flight paths
          theMartianJourney = dataSource.entities.getById("TheMartianJourney");
          theMartianJourney.polyline.width = createWidthCallbackProperty(
            new Cesium.NearFarScalar(0.0, 10.0, 1.0e7, 0.0),
          );
          theMartianJourney.rectangle.material = new Cesium.ImageMaterialProperty({
            image: createCanvasAsTexture('Mark Watney\'s Journey in "The Martian"'),
            transparent: true,
          });

          roverMenuEntries.push({
            text: '"The Martian" Journey',
            onselect: () => {
              reset();
              viewer.zoomTo(theMartianJourney);
            },
          });

          Sandcastle.addToolbarMenu(roverMenuEntries);
        } catch (error) {
          console.log(`Error loading CZML: ${error}`);
        }

        // For changing the width of polylines based on distance from the camera
        function createWidthCallbackProperty(nearFarScalar) {
          return new Cesium.CallbackProperty(function () {
            const distance = viewer.camera.positionCartographic.height;
            let t =
              (distance - nearFarScalar.near) / (nearFarScalar.far - nearFarScalar.near);
            t = Cesium.Math.clamp(t, 0.0, 1.0);
            return Cesium.Math.lerp(nearFarScalar.nearValue, nearFarScalar.farValue, t);
          }, false);
        }

        // Converts a Julian date to a Mars Sol number, given a start date / sol number
        function createJulianDateToSolConverter(startJulianDate, startSol) {
          return function (julianDate) {
            const secondsPerSol = 24 * 60 * 60 + 39 * 60 + 35;
            const differenceInSeconds = Cesium.JulianDate.secondsDifference(
              julianDate,
              startJulianDate,
            );
            const solNumber = Math.floor(differenceInSeconds / secondsPerSol) + startSol;
            return `Sol ${solNumber}`;
          };
        }

        // Load points of interest from GeoJSON data source
        try {
          const dataSource = await Cesium.GeoJsonDataSource.load(
            "../../SampleData/MarsPointsOfInterest.geojson",
          );
          viewer.dataSources.add(dataSource);

          const onSelectLandmark = (landmark) => {
            reset();
            scene.camera.flyTo(landmark);
          };

          const landmarkMenuEntries = [
            {
              text: "Fly to landmark...",
              onselect: () => {},
            },
          ];

          const entities = dataSource.entities.values;
          entities.forEach((entity) => {
            entity.label = new Cesium.LabelGraphics({
              text: entity.properties.text,
              font: "18pt Verdana",
              outlineColor: Cesium.Color.DARKSLATEGREY,
              outlineWidth: 2,
              style: Cesium.LabelStyle.FILL_AND_OUTLINE,
              pixelOffset: new Cesium.Cartesian2(0, -22),
              scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.5),
              translucencyByDistance: new Cesium.NearFarScalar(2.5e7, 1.0, 4.0e7, 0.0),
              heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
              disableDepthTestDistance: new Cesium.CallbackProperty(() => {
                return Cesium.Cartesian3.magnitude(scene.camera.positionWC);
              }, false),
            });

            entity.point = new Cesium.PointGraphics({
              pixelSize: 10,
              color: Cesium.Color.fromBytes(243, 242, 99),
              outlineColor: Cesium.Color.fromBytes(219, 218, 111),
              outlineWidth: 2,
              scaleByDistance: new Cesium.NearFarScalar(1.5e3, 1.0, 4.0e7, 0.1),
              heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
              disableDepthTestDistance: new Cesium.CallbackProperty(() => {
                return Cesium.Cartesian3.magnitude(scene.camera.positionWC);
              }, false),
            });

            entity.name = entity.properties.text.getValue();
            entity.description = createPickedFeatureDescription(entity);

            const flyToDestination = new Cesium.Cartesian3.fromArray(
              entity.properties.destination.getValue(),
            );
            const orientationArray = entity.properties.orientation.getValue();
            const flyToOrientation = new Cesium.HeadingPitchRoll(
              orientationArray[0],
              orientationArray[1],
              orientationArray[2],
            );

            landmarkMenuEntries.push({
              text: entity.properties.text.getValue(),
              onselect: () =>
                onSelectLandmark({
                  destination: flyToDestination,
                  orientation: flyToOrientation,
                  easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
                  maximumHeight: 5e6,
                  pitchAdjustHeight: 2.5e6,
                  duration: 3.0,
                  complete: function () {
                    viewer.selectedEntity = entity;
                    viewer.infoBox.viewModel.showInfo = true;
                  },
                }),
            });
          });

          Sandcastle.addToolbarMenu(landmarkMenuEntries);
        } catch (error) {
          console.log(`Error loading GeoJSON: ${error}`);
        }

        // Spin Mars on first load but disable the spinning upon any input
        const rotationSpeed = Cesium.Math.toRadians(0.1);
        const removeRotation = viewer.scene.postRender.addEventListener(
          function (scene, time) {
            viewer.scene.camera.rotateRight(rotationSpeed);
          },
        );

        const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
        handler.setInputAction(
          () => removeRotation(),
          Cesium.ScreenSpaceEventType.LEFT_DOWN,
        );
        handler.setInputAction(
          () => removeRotation(),
          Cesium.ScreenSpaceEventType.RIGHT_DOWN,
        );
        handler.setInputAction(
          () => removeRotation(),
          Cesium.ScreenSpaceEventType.MIDDLE_DOWN,
        );
        handler.setInputAction(() => removeRotation(), Cesium.ScreenSpaceEventType.WHEEL);

        // For drawing attention to the play button in the animation view model
        function highlightAnimationViewModel() {
          if (clock.shouldAnimate) {
            // Animation already playing
            return;
          }

          const playPath =
            viewer.animation.container.querySelector("#animation_pathPlay");
          const playButton = playPath.closest("g.cesium-animation-rectButton");
          const ringG = viewer.animation.container.querySelector(
            ".cesium-animation-shuttleRingG",
          );
          playButton.classList.add("highlight-animation");
          ringG.classList.add("highlight-animation");

          playButton.addEventListener("click", removeHighlight, { once: true });
          setTimeout(removeHighlight, 30000); // Remove after 30 seconds if not clicked
        }

        function removeHighlight() {
          const playPath =
            viewer.animation.container.querySelector("#animation_pathPlay");
          const playButton = playPath.closest("g.cesium-animation-rectButton");
          const ringG = viewer.animation.container.querySelector(
            ".cesium-animation-shuttleRingG",
          );
          playButton.classList.remove("highlight-animation");
          ringG.classList.remove("highlight-animation");
        }

        function reset() {
          clock.multiplier = 1;
          viewer.selectedEntity = undefined;
          viewer.trackedEntity = undefined;
          viewer.timeline.zoomTo(clock.startTime, clock.stopTime);
          removeRotation();
          removeHighlight();
        }

        // Add a listener for when the home button is clicked.
        viewer.homeButton.viewModel.command.beforeExecute.addEventListener(
          function (commandInfo) {
            reset();
          },
        );

        // When animating, if the multiplier is very high (which is necessary to see rover movement),
        // model lighting flickers distractingly, so disable it
        const entitiesToDisableLightingFor = [curiosity, perseverance, ingenuity];
        Cesium.knockout
          .getObservable(viewer.clockViewModel, "shouldAnimate")
          .subscribe(function (shouldAnimate) {
            if (shouldAnimate && clock.multiplier >= 100000) {
              entitiesToDisableLightingFor.forEach(function (entity) {
                entity.model.lightColor = new Cesium.Color(0, 0, 0);
              });
            } else {
              entitiesToDisableLightingFor.forEach(function (entity) {
                entity.model.lightColor = new Cesium.Color(1, 1, 1);
              });
            }
          });

        // To create a rectangle with text that conforms to the terrain, we can create a canvas
        // with text and use it as a texture on a rectangle entity.
        function createCanvasAsTexture(text) {
          const canvas = document.createElement("canvas");
          canvas.width = 1024;
          canvas.height = 256;

          const ctx = canvas.getContext("2d");
          // Background
          ctx.fillStyle = "rgba(0, 0, 0, 0)";
          ctx.fillRect(0, 0, canvas.width, canvas.height);

          // Text
          ctx.font = "36px sans-serif";
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.strokeStyle = "rgba(0,0,0,0.1)";
          ctx.lineWidth = 1;
          ctx.fillStyle = "#ffffff";

          ctx.strokeText(text, canvas.width / 2, canvas.height / 2);
          ctx.fillText(text, canvas.width / 2, canvas.height / 2);

          return canvas;
        }

        // Create the HTML that will be put into the info box that shows
        // information about the currently selected feature
        function createPickedFeatureDescription(entity) {
          return `<img\
              width="50%"\
              style="float:left; margin: 0 1em 1em 0;"\
              src=${entity.properties.imageURL}>\
            <p>${entity.properties.description}</p>\
            <p>\
              Source: \
              <a style="color: WHITE"\
                target="_blank"\
                href="${entity.properties.sourceURL}">${entity.properties.source}</a>\
            </p>`;
        }

        // Inject instructions for interacting with the rovers into the navigation help menu
        function addRoverInstructionsToNavMenu() {
          const div = document.querySelector(
            ".cesium-click-navigation-help.cesium-navigation-help-instructions",
          );
          const table = div.querySelector("table");

          const instructions1 = document.getElementById("roverHelpRowTemplate1");
          const instructions1Clone = instructions1.content.cloneNode(true);
          const img = instructions1Clone.querySelector("img[data-src]");
          img.src = Cesium.buildModuleUrl(img.dataset.src);
          table.tBodies[0].appendChild(instructions1Clone);

          const instructions2 = document.getElementById("roverHelpRowTemplate2");
          const instructions2Clone = instructions2.content.cloneNode(true);
          table.tBodies[0].appendChild(instructions2Clone);
        }
        addRoverInstructionsToNavMenu();
        //Sandcastle_End
      };
      if (typeof Cesium !== "undefined") {
        window.startupCalled = true;
        window.startup(Cesium).catch((error) => {
          "use strict";
          console.error(error);
        });
        Sandcastle.finishedLoading();
      }
    </script>
  </body>
</html>
